{
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
}

unit QuestionScore;

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, ExtCtrls, StdCtrls, TestSession,
  TestCore, MiscUtils, Graphics, ComCtrls, ActnList, VisualUtils, NavigationDiagram,
  Math, Profile, QuestionScreens, TestUiUtils;

type
  TQuestionScoreFrame = class(TFrame, INavigationDiagramHost{ internal })
    actBack: TAction;
    actAdvance: TAction;
    alMain: TActionList;
    bvlNavigator: TBevel;
    bvlDetails: TBevel;
    bvlTop: TBevel;
    cbxSections: TComboBox;
    ilMain: TImageList;
    lblSections: TLabel;
    lblSection: TLabel;
    lblSectionTitle: TLabel;
    lblWeightRight: TLabel;
    lblWeightRightTitle: TLabel;
    lblWeight: TLabel;
    lblWeightTitle: TLabel;
    lblPartRight: TLabel;
    lblPartRightTitle: TLabel;
    pnlBottom: TPanel;
    pnlDetails: TPanel;
    pnlDiagram: TPanel;
    pnlMain: TPanel;
    pnlNavigator: TPanel;
    pnlTop: TPanel;
    tbAdvance: TToolButton;
    tbBack: TToolButton;
    tbrAnswerSwitch: TToolBar;
    tbrNavigator: TToolBar;
    tbSpacer: TToolButton;
    tbUserAnswer: TToolButton;
    tbCorrectAnswer: TToolButton;
    procedure actAdvanceExecute(Sender: TObject);
    procedure actAdvanceUpdate(Sender: TObject);
    procedure actBackExecute(Sender: TObject);
    procedure actBackUpdate(Sender: TObject);
    procedure cbxSectionsChange(Sender: TObject);
    procedure tbCorrectAnswerClick(Sender: TObject);
    procedure tbUserAnswerClick(Sender: TObject);
  private
    FSessionResult: TSessionResult;
    FQuestions: TQuestionList;
    FSelectedSectionIndex: Integer;
    FScreen: TFrame;
    FShowRightAnswer: Boolean;
    FDiagram: TNavigationDiagramFrame;
    FOnSelectSection: TNotifyEvent;
    FMaxWeight: Integer;
    procedure SetSelectedSectionIndex(Value: Integer);
    procedure ReleaseScreen;
    function GetAdjacentQuestion(Offset: Integer): Integer;
    procedure DisplayPartRight(Value: Single);
    procedure DisplayWeight(Value: Integer);
    procedure DisplayWeightRight(const Value: Double);
    procedure ShowCurrentQuestionDetails;
    procedure PrepareDetailsPanel;
    procedure SetShowRightAnswer(Value: Boolean);
    function SelectedQuestionIndex: Integer;
    { private declarations }
  public
    destructor Destroy; override;
    procedure SetUp(SessionResult: TSessionResult; Questions: TQuestionList);
    procedure SelectQuestion(Index: Integer);

    { INavigationDiagramHost (internal) }
    function GetItemColor(ItemIndex: Integer): TColor;
    function GetItemHint(ItemIndex: Integer): String;
    procedure ItemSelected(ItemIndex: Integer);

    property OnSelectSection: TNotifyEvent read FOnSelectSection write FOnSelectSection;
    property SelectedSectionIndex: Integer read FSelectedSectionIndex write SetSelectedSectionIndex;
    { public declarations }
  end;

implementation

{$R *.lfm}
{$R *.rc}

resourcestring
  SAll = 'All';

const
  WEIGHT_RIGHT_PRECISION = 2;

{ TQuestionScoreFrame }

procedure TQuestionScoreFrame.tbCorrectAnswerClick(Sender: TObject);
begin
  SetShowRightAnswer(TRUE);
end;

procedure TQuestionScoreFrame.tbUserAnswerClick(Sender: TObject);
begin
  SetShowRightAnswer(FALSE);
end;

procedure TQuestionScoreFrame.actBackExecute(Sender: TObject);
begin
  SelectQuestion(GetAdjacentQuestion(-1));
end;

procedure TQuestionScoreFrame.actBackUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := (Parent <> nil) and Parent.IsControlVisible
    and (GetAdjacentQuestion(-1) <> -1);
end;

procedure TQuestionScoreFrame.cbxSectionsChange(Sender: TObject);
begin
  if cbxSections.ItemIndex <> -1 then
    SelectedSectionIndex := cbxSections.ItemIndex - 1;
end;

procedure TQuestionScoreFrame.actAdvanceExecute(Sender: TObject);
begin
  SelectQuestion(GetAdjacentQuestion(1));
end;

procedure TQuestionScoreFrame.actAdvanceUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := (Parent <> nil) and Parent.IsControlVisible
    and (GetAdjacentQuestion(1) <> -1);
end;

procedure TQuestionScoreFrame.SetSelectedSectionIndex(Value: Integer);
var
  k: Integer;
begin
  if FSelectedSectionIndex <> Value then
  begin
    FSelectedSectionIndex := Value;
    cbxSections.ItemIndex := FSelectedSectionIndex + 1;
    if (FSelectedSectionIndex <> -1) and
      (FSessionResult.QuestionResults[SelectedQuestionIndex].SectionIndex <> FSelectedSectionIndex) then
    begin
      k := GetAdjacentQuestion(1);
      if k = -1 then
      begin
        k := GetAdjacentQuestion(-1);
        Assert( k <> -1 );
      end;
      SelectQuestion(k);
    end;
    FDiagram.ColorsChanged;

    if Assigned(FOnSelectSection) then
      FOnSelectSection(Self);
  end;
end;

procedure TQuestionScoreFrame.ReleaseScreen;
begin
  if FScreen <> nil then
  begin
    FScreen.Parent := nil;
    Application.ReleaseComponent(FScreen);
    FScreen := nil;
  end;
end;

function TQuestionScoreFrame.GetAdjacentQuestion(Offset: Integer): Integer;
var
  k: Integer;
begin
  k := SelectedQuestionIndex + Offset;
  while (k >= 0) and (k < FSessionResult.QuestionResultCount) and
    (FSelectedSectionIndex <> -1) and
    (FSessionResult.QuestionResults[k].SectionIndex <> FSelectedSectionIndex) do
    k := k + Offset;
  if (k >= 0) and (k < FSessionResult.QuestionResultCount) then
    Result := k
  else
    Result := -1;
end;

procedure TQuestionScoreFrame.DisplayPartRight(Value: Single);
begin
  lblPartRight.Caption := Format(' %d%%', [Round(Value*100)]);
end;

procedure TQuestionScoreFrame.DisplayWeight(Value: Integer);
begin
  lblWeight.Caption := Format(' %d', [Value]);
end;

procedure TQuestionScoreFrame.DisplayWeightRight(const Value: Double);
begin
  lblWeightRight.Caption := ' ' + ConvertFloatToString(Value,
    WEIGHT_RIGHT_PRECISION, GetUiDecimalSeparator);
end;

procedure TQuestionScoreFrame.ShowCurrentQuestionDetails;
var
  QuestionResult: TQuestionResult;
  PartRight: Single;
  WeightRight: Double;
begin
  QuestionResult := FSessionResult.QuestionResults[SelectedQuestionIndex];

  if lblPartRight.Visible then
  begin
    if QuestionResult.UserAnswer = nil then
      PartRight := 0
    else
      PartRight := QuestionResult.PartRight;
    DisplayPartRight(PartRight);
  end;

  if lblWeight.Visible then
    DisplayWeight(QuestionResult.Weight);

  if lblWeightRight.Visible then
  begin
    if QuestionResult.UserAnswer = nil then
      WeightRight := 0
    else
      WeightRight := QuestionResult.Weight * QuestionResult.PartRight;
    DisplayWeightRight(WeightRight);
  end;

  if lblSection.Visible then
    lblSection.Caption := ' ' + FSessionResult.SectionResults[QuestionResult.SectionIndex].Name;
end;

procedure TQuestionScoreFrame.PrepareDetailsPanel;
var
  WeightRightAvailable, SectionAvailable: Boolean;
  p: TResultPolicy;
begin
  DisplayPartRight(1);
  lblPartRight.Constraints.MinWidth := GetPreferredControlSize(lblPartRight).cx;

  DisplayWeight(FMaxWeight);
  lblWeight.Constraints.MinWidth := GetPreferredControlSize(lblWeight).cx;

  DisplayWeightRight(FMaxWeight - power(10, -WEIGHT_RIGHT_PRECISION));
  lblWeightRight.Constraints.MinWidth := GetPreferredControlSize(lblWeightRight).cx;

  SetRightBorderSpacing([lblPartRight, lblWeight, lblWeightRight], 20);

  p := FSessionResult.Policy;

  lblPartRightTitle.Visible := p.QuestionPercentCorrect;
  lblPartRight.Visible := p.QuestionPercentCorrect;

  lblWeightTitle.Visible := p.QuestionPoints;
  lblWeight.Visible := p.QuestionPoints;

  WeightRightAvailable := p.QuestionPoints and p.QuestionPercentCorrect;
  lblWeightRightTitle.Visible := WeightRightAvailable;
  lblWeightRight.Visible := WeightRightAvailable;

  SectionAvailable := p.SectionResultsAvailable
    and (FSessionResult.SectionResultCount > 1) and p.SectionQuestionList;
  lblSectionTitle.Visible := SectionAvailable;
  lblSection.Visible := SectionAvailable;

  if not HasVisibleChildControls(pnlDetails) then
  begin
    pnlDetails.Visible := FALSE;
    bvlDetails.Visible := FALSE;
  end;
end;

procedure TQuestionScoreFrame.SetShowRightAnswer(Value: Boolean);
begin
  if FShowRightAnswer <> Value then
  begin
    FShowRightAnswer := Value;
    SelectQuestion(SelectedQuestionIndex);
  end;
end;

function TQuestionScoreFrame.SelectedQuestionIndex: Integer;
begin
  Result := FDiagram.SelectedItemIndex;
end;

function TQuestionScoreFrame.GetItemColor(ItemIndex: Integer): TColor;
var
  QuestionResult: TQuestionResult;
begin
  QuestionResult := FSessionResult.QuestionResults[ItemIndex];
  if (FSelectedSectionIndex = -1) or (FSelectedSectionIndex = QuestionResult.SectionIndex) then
  begin
    if QuestionResult.UserAnswer = nil then
      Result := TDiagramColorScheme.UnansweredColor
    else if FSessionResult.Policy.QuestionPercentCorrect then
      Result := TDiagramColorScheme.GetColorForPartRight(QuestionResult.PartRight)
    else
      Result := TDiagramColorScheme.AnsweredColor;
  end
  else { the question is not in the selected section }
    Result := clNone;
end;

function TQuestionScoreFrame.GetItemHint(ItemIndex: Integer): String;
begin
  Result := FQuestions[ItemIndex].Title;
end;

procedure TQuestionScoreFrame.ItemSelected(ItemIndex: Integer);
begin
  if (FSelectedSectionIndex <> -1) and
    (FSessionResult.QuestionResults[ItemIndex].SectionIndex <> FSelectedSectionIndex) then
    SelectedSectionIndex := -1;
  SelectQuestion(ItemIndex);
end;

procedure TQuestionScoreFrame.SelectQuestion(Index: Integer);
var
  QuestionResult: TQuestionResult;
  Question: TQuestion;
  Answer: TResponse;
  Screen: IQuestionScreen;
begin
  LockRedraw(pnlMain);
  try
    ReleaseScreen;
    try
      FDiagram.SelectedItemIndex := Index;

      QuestionResult := FSessionResult.QuestionResults[SelectedQuestionIndex];
      Question := FQuestions[SelectedQuestionIndex];

      if FShowRightAnswer then
        Answer := QuestionResult.RightAnswer
      else if QuestionResult.UserAnswer = nil then
        Answer := QuestionResult.EmptyAnswer
      else
        Answer := QuestionResult.UserAnswer;
      Question.Response.Assign(Answer);

      FScreen := QuestionScreenRegistry.Find(
        TQuestionClass(Question.ClassType)).Create(nil);
      Screen := FScreen as IQuestionScreen;

      FScreen.Align := alClient;
      FScreen.Parent := pnlMain;
      Screen.SetUp(Question);
      Screen.GoReadOnly;

      ShowCurrentQuestionDetails;
    except
      ReleaseScreen;
    end;
  finally
    UnlockRedraw(pnlMain);
  end;
end;

destructor TQuestionScoreFrame.Destroy;
begin
  ReleaseScreen;
  FreeAndNil(FDiagram);
  FreeAndNil(FQuestions);
  inherited;
end;

procedure TQuestionScoreFrame.SetUp(SessionResult: TSessionResult;
  Questions: TQuestionList);
var
  q: TQuestion;
  i: Integer;
  Weights: TIntegerArray;
begin
  HandleNeeded;
  cbxSections.Items.Add('(' + SAll + ')');

  LoadActionImages(alMain);
  tbUserAnswer.ImageIndex := AddPngResourceToImageList('QuestionScore/userAnswer.png', ilMain);
  tbCorrectAnswer.ImageIndex := AddPngResourceToImageList('QuestionScore/correctAnswer.png', ilMain);

  StretchToWidest([tbBack, tbAdvance]);

  FSessionResult := SessionResult;
  FQuestions := TQuestionList.Create;
  for q in Questions do
    FQuestions.AddSafely(q.Clone);
  Assert( FSessionResult.QuestionResultCount = FQuestions.Count );

  SetLength(Weights, FSessionResult.QuestionResultCount);
  for i := 0 to High(Weights) do
  begin
    Weights[i] := FSessionResult.QuestionResults[i].Weight;
    FMaxWeight := Max(FMaxWeight, Weights[i]);
  end;
  FDiagram := TNavigationDiagramFrame.Embed(Weights, Self, pnlDiagram);

  tbrAnswerSwitch.Visible := FSessionResult.Policy.QuestionCorrectAnswer;

  PrepareDetailsPanel;

  FSelectedSectionIndex := -1;
  cbxSections.ItemIndex := 0;
  if FSessionResult.Policy.SectionResultsAvailable
    and (FSessionResult.SectionResultCount > 1)
    and FSessionResult.Policy.SectionQuestionList then
  begin
    for i := 0 to FSessionResult.SectionResultCount-1 do
      cbxSections.Items.Add(FSessionResult.SectionResults[i].Name);
  end
  else
  begin
    lblSections.Visible := FALSE;
    cbxSections.Visible := FALSE;
  end;
end;

end.

